終於到了最後一個範例,在 第一天的文章 - 前言 提到最近工作上使用到 opencv.js,所以才有這 30 天的 web worker 系列文,有始有終,今天就讓我們來完成一開始打算做的事情 - 在 web worker 中使用 opencv.js
前情提要一下什麼是 opencv.js
opencv.js 是 OpenCV(Open Source Computer Vision Library) 的 JavaScript 版本。 它是 OpenCV 函式庫的子項目,透過使用 Emscripten 技術,將原始的 C++ 程式碼編譯成 JavaScript,使得開發者可以在瀏覽器中直接執行 OpenCV 的功能。 OpenCV 提供了一些圖像處理的功能,例如:濾波、邊緣檢測、顏色轉換、人臉識別等
進一步瞭解可以參考 官方 opencv.js 教學
結合 web worker 與 opencv.js,比較 有無使用 web worker 效能的差異
範例 Demo
圖片很大,第一次進去可能會載入比較久,請耐心等候
在使用者拉動 slider 時,會根據改變的值 (0~100%) 當作參數去尋找圖中的星星
document.querySelector('#slider').addEventListener('change', async (e) => {
const checkbox = document.querySelector('#use-worker');
const isUsingWorker = checkbox.checked;
// 拖動範圍 0 ~ 100%
const percent = e.detail;
if (isUsingWorker) {
// 使用 web worker 尋找星星
await findStarsWithWorker(percent);
} else {
// 不使用 web worker 尋找星星
findStarsWithoutWorker(percent);
}
});
先來看 不使用 worker 找星星 的部分
在 opencv 中可以使用 cv.imread()
,將圖片或是 canvas 讀進來,這裡帶入 canvas 元件 id (#canvasInput) 可以把 canvas 中的圖像載入進 opencv 裡做使用
接著主要是利用 cv.findContours,找尋星星的輪廓標記成綠色亮點,這部分因為不是今天強調的重點就不仔細說明了,大致上類似以下兩篇文章的實作方式:
當找出星星後,會將其標記成綠色亮點,接著將 結果圖像(outputMat) 利用 cv.imshow()
,顯示在 canvas 上(#canvasOutput)
export const findStarsWithoutWorker = (percent = 0) => {
// 讀入原始星空圖
const inputMat = cv.imread('canvasInput');
// 初始 percent 為 0,不做任何處理回傳原始圖
if (!percent) {
cv.imshow('canvasOutput', inputMat);
inputMat.delete();
return;
}
// 使用 opencv 尋找星星邏輯 (省略...)
// ...
// 在瀏覽器中顯示結果圖像
cv.imshow('canvasOutput', outputMat);
};
由於使用 cv.imread()
載入圖像後回傳的是 opencv 特有的 cv.Mat
資料格式,而 cv.Mat
是無法經由 postMessage
的 structuredClone
算法傳遞到 worker 線程中的
// 主線程
const inputMat = cv.imread('canvasInput');
worker.postMessage(inputMat);
// worker 線程
self.onmessage = (e) => {
// cv.Mat 資料無法被複製
console.log(e.data); // {}
}
因此我們需要先將 cv.Mat
使用 matToImageData
函式轉換為 第 14 天所提到的 ImageData,才能使用 postMessage
傳遞資料,注意這裡 postMessage
有設定第二個參數將 ImageData
的 buffer
轉移到 worker 線程,略過複製讓資料的傳遞更快
const matToImageData = (mat) => {
return new ImageData(new Uint8ClampedArray(mat.data), mat.cols, mat.rows);
};
export const findStarsWithWorker = (percent) => {
return new Promise((resolve) => {
const inputMat = cv.imread('canvasInput');
const imageData = matToImageData(inputMat);
// 釋放記憶體
inputMat.delete();
worker.postMessage(
{
imageData,
percent
},
[imageData.data.buffer]
);
});
};
以上程式碼有一行 inputMat.delete()
拿來釋放不會用到的 cv.Mat
記憶體,我一開始忘記加到這行,當 slider 拖拉久了之後,可以看到 console 一直噴錯,不斷印出一串數字,當加了 inputMat.delete()
正確釋放記憶體後就沒有這個問題了
worker 線程接收到 ImageData
後需要轉換回 cv.Mat
的形式,才能使用 opencv 運算,這裡我們可以使用 opencv 內建的 cv.matFromImageData()
轉換,接下來的部分跟 不使用 web worker 尋找星星(findStarsWithoutWorker) 一樣,差別只在運算完後需要再把 cv.Mat
轉換成 ImageData
傳回主線程
// worker 線程
self.onmessage = (e) => {
findStarsInWorker(e.data);
};
function findStarsInWorker({ imageData, percent = 0 }) {
const inputMat = cv.matFromImageData(imageData);
if (!percent) {
// 將處理後的圖像資料傳回主線程
const processedImageData = matToImageData(inputMat);
self.postMessage({ imageData: processedImageData }, [
processedImageData.data.buffer
]);
// 釋放記憶體
inputMat.delete();
return;
}
// 使用 opencv 尋找星星邏輯 (省略...)
// ...
// 將處理後的圖像資料傳回主線程
const processedImageData = matToImageData(outputMat);
self.postMessage({ imageData: processedImageData }, [
processedImageData.data.buffer
]);
}
最終主線程接收到 ImageData
後使用 putImageData
就可以畫在 canvas 上
export const findStarsWithWorker = (percent) => {
return new Promise((resolve) => {
worker.onmessage = (e) => {
const outputCanvas = document.querySelector('#canvasOutput');
outputCanvas.getContext('2d').putImageData(e.data.imageData, 0, 0);
resolve();
};
});
};
使用 opencv.js 可以在網頁上做到圖像辨識、邊緣偵測等許多功能,但也因為其對影像處理的強大,使用 opencv.js 常會需要更多的運算時間,當運算的時間長到會影響到畫面上的操作時,可以考慮將相關的運算移到 worker 線程中處理,優化使用者在網頁上的操作體驗
Getting Started with Images
学习opencv.js(1)图像入门
Contours : Getting Started
二值化黑白影像